﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System.IO.Pipes;
using System.Runtime.Versioning;
using System.Security.AccessControl;
using System.Security.Principal;
using Microsoft.DotNet.Cli.Utils;

namespace Microsoft.DotNet.Cli.Installer.Windows;

/// <summary>
/// Defines some generic security related helper methods.
/// </summary>
[SupportedOSPlatform("windows")]
internal static class SecurityUtils
{
    /// <summary>
    /// Default inheritance to apply to directory ACLs.
    /// </summary>
    private static readonly InheritanceFlags s_DefaultInheritance = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit;

    /// <summary>
    /// SID that matches built-in administrators.
    /// </summary>
    private static readonly SecurityIdentifier s_AdministratorsSid = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null);

    /// <summary>
    /// SID that matches everyone.
    /// </summary>
    private static readonly SecurityIdentifier s_EveryoneSid = new SecurityIdentifier(WellKnownSidType.WorldSid, null);

    /// <summary>
    /// Local SYSTEM SID.
    /// </summary>
    private static readonly SecurityIdentifier s_LocalSystemSid = new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null);

    /// <summary>
    /// SID matching built-in user accounts.
    /// </summary>
    private static readonly SecurityIdentifier s_UsersSid = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);

    /// <summary>
    /// ACL rule associated with the Administrators SID.
    /// </summary>
    private static readonly FileSystemAccessRule s_AdministratorRule = new FileSystemAccessRule(s_AdministratorsSid, FileSystemRights.FullControl,
        s_DefaultInheritance, PropagationFlags.None, AccessControlType.Allow);

    /// <summary>
    /// ACL rule associated with the Everyone SID.
    /// </summary>
    private static readonly FileSystemAccessRule s_EveryoneRule = new FileSystemAccessRule(s_EveryoneSid, FileSystemRights.ReadAndExecute,
        s_DefaultInheritance, PropagationFlags.None, AccessControlType.Allow);

    /// <summary>
    /// ACL rule associated with the Local SYSTEM SID.
    /// </summary>
    private static readonly FileSystemAccessRule s_LocalSystemRule = new FileSystemAccessRule(s_LocalSystemSid, FileSystemRights.FullControl,
        s_DefaultInheritance, PropagationFlags.None, AccessControlType.Allow);

    /// <summary>
    /// ACL rule associated with the built-in users SID.
    /// </summary>
    private static readonly FileSystemAccessRule s_UsersRule = new FileSystemAccessRule(s_UsersSid, FileSystemRights.ReadAndExecute,
        s_DefaultInheritance, PropagationFlags.None, AccessControlType.Allow);

    /// <summary>
    /// Creates the specified directory and secures it by configuring access rules (ACLs) that allow sub-directories
    /// and files to inherit access control entries. 
    /// </summary>
    /// <param name="path">The path of the directory to create.</param>
    public static void CreateSecureDirectory(string path)
    {
        if (!Directory.Exists(path))
        {
            DirectorySecurity ds = new();
            SetDirectoryAccessRules(ds);
            ds.CreateDirectory(path);
        }
    }

    /// <summary>
    /// Moves a file from one location to another if the destination file does not already exist and
    /// configure its permissions.
    /// </summary>
    /// <param name="sourceFile">The source file to move.</param>
    /// <param name="destinationFile">The destination where the source file will be moved.</param>
    /// <param name="log">The underlying setup log to use.</param>
    public static void MoveAndSecureFile(string sourceFile, string destinationFile, ISetupLogger log = null)
    {
        if (!File.Exists(destinationFile))
        {
            FileAccessRetrier.RetryOnMoveAccessFailure(() =>
            {
                // Moving the file preserves the owner SID and fails to inherit the WD ACE.
                File.Copy(sourceFile, destinationFile, overwrite: true);
                File.Delete(sourceFile);
            });
            log?.LogMessage($"Moved '{sourceFile}' to '{destinationFile}'");

            SecureFile(destinationFile);
        }
    }

    /// <summary>
    /// Secures a file by setting the owner and group to built-in administrators (BA). All other ACE values are inherited from
    /// the parent directory.
    /// </summary>
    /// <param name="path">The path of the file to secure.</param>
    public static void SecureFile(string path)
    {
        FileInfo fi = new(path);
        FileSecurity fs = new();

        // See https://github.com/dotnet/sdk/issues/28450. If the directory's descriptor
        // is correctly configured, we should end up with an inherited ACE for Everyone: (A;ID;0x1200a9;;;WD)
        fs.SetOwner(s_AdministratorsSid);
        fs.SetGroup(s_AdministratorsSid);
        fi.SetAccessControl(fs);
    }

    /// <summary>
    /// Apply a standard set of access rules to the directory security descriptor. The owner and group will
    /// be set to built-in Administrators. Full access is granted to built-in administators and SYSTEM with
    /// read, execute, synchronize permssions for built-in users and Everyone.
    /// </summary>
    /// <param name="ds">The security descriptor to update.</param>
    private static void SetDirectoryAccessRules(DirectorySecurity ds)
    {
        ds.SetOwner(s_AdministratorsSid);
        ds.SetGroup(s_AdministratorsSid);
        ds.SetAccessRule(s_AdministratorRule);
        ds.SetAccessRule(s_LocalSystemRule);
        ds.SetAccessRule(s_UsersRule);
        ds.SetAccessRule(s_EveryoneRule);
    }
}
